/*
 *  WsvJdbc - a JDBC driver for WSV (values separated by whatever) data files
 *  Copyright (C) 2004  Daniele Pes
 * 
 *  CsvJdbc - a JDBC driver for CSV files
 *  Copyright (C) 2001  Jonathan Ackerman
 * 
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
 
package axs.jdbc.driver;

import java.util.StringTokenizer;
import java.util.Vector;

import axs.jdbc.dataSourceConfiguration.JdbcSourceConfiguration;
import axs.jdbc.dataSpecification.SqlCounterFakeType;
import axs.jdbc.dataSpecification.SqlEveryRecordFakeType;
import axs.jdbc.utils.StringUtilities;


/**
 * This is a very crude and SQL simple parser used by the Wsv JDBC driver. It
 * only handles SELECT statements in the format "SELECT xxx,yyy,zzz FROM fffff".
 * It also handles SELECT * FROM table and SELECT COUNT(*) FROM table.
 *
 * @author     Jonathan Ackerman
 * @author     Daniele Pes
 * @created    25 November 2001
 * @version    $Id: WsvSqlParser.java,v 1.2 2007/04/16 17:24:36 montag451 Exp $
 */
public class WsvSqlParser {

   private String    tableName;     
   private String[]  columnNames; 	
	private Class[]   columnTypes; 			        
	private boolean   colsIncludeAtLeastAcounter;  
	private Integer[] stringTypedColumnWidths;	  
	private Integer[] columnBeginBounds;			  
	private Integer[] columnEndBounds;   			  
	private String    separator;
   private boolean[] trimIfString;
	
	
   /**
    *Gets the tableName attribute of the SqlParser object
    *
    * @return    The tableName value
    * @since
    */
   public String getTableName() {
      return tableName;
   }
   
   
   /**
    *Gets the columnNames attribute of the SqlParser object
    *
    * @return    The columnNames value
    * @since
    */
   public String[] getColumnNames() {
      return columnNames;
   }
   
   
	/**
	 *Gets the columnTypes attribute of the SqlParser object
	 *
	 * @return    The columnTypes value
	 * @since
	 */	
	public Class[] getColumnTypes() {
		return columnTypes;
	}
	
	
	public Class getColumnTypeByName(String   columnName,
												String[] columnNames,
												Class[]  columnTypes){
		Class result;
		//
		result = String.class;  // default column type
		if (columnTypes != null) {
			int i = -1;
			for (i = 0; i < columnNames.length; i++) {
				if (columnNames[i].equals(columnName))
					break;
			}
			result = columnTypes[i];
		}
		return result;
	}
	
		
	public Integer getColumnBeginBoundByName(String columnName) {
		return getBoundByName(columnName, "begin");				
	}

		
	public Integer getColumnEndBoundByName(String columnName) {
		return getBoundByName(columnName, "end");
	}
	
	
	private Integer getBoundByName(String columnName,
											 String type) {
		Integer   result;
		Integer[] columnBounds;
		//
		result = null;
		columnBounds = type.equals("begin") ? columnBeginBounds : columnEndBounds;
		if (columnBounds != null) {
			int i = -1;
			for (i = 0; i < columnNames.length; i++) {
				if (columnNames[i].equals(columnName))
					break;
			}
			result = columnBounds[i];
		}
		return result;
	}
	
		
	public boolean haveRecordsToBeCounted(){
		return colsIncludeAtLeastAcounter;
	}
	
	
   /**
    * Analyzes the passed SQL statement to:
    * (1) check for SELECT.
    * (2) check for FROM.
    * (3) check for COUNT(*).
    * (4) gathers names of columns.
    *
    * @param      sql        The SQL statement.
    * @exception  Exception  Thrown if it's a malformed command.
    */
   public void parseSyntax(String sql) 
	throws Exception {
		int fromPos;
		//
      tableName   = null;            	
      if (!sql.startsWith("SELECT ")) {
         throw new Exception("Malformed SQL. Missing SELECT statement.");
      }
		fromPos = sql.lastIndexOf(" FROM "); 
      if (fromPos == -1) {
         throw new Exception("Malformed SQL. Missing FROM statement.");
      }
				            
   }


	/**
	 * Analyzes the SQL statement 'sql' to verify its sintactical correctness. It also identifies the name of table,
	 * the names of table columns and their types. Finally, it finds out wether, among the columns to be selected, there
	 * is any COUNT clause.
	 *  
	 * @pre_condition It's assumed that conn.columnNames and conn.columnTypes are well defined (not empty) and coherent 
	 * 				   (same number of elements).
	 * @param  sql       The SQL statement to be parsed.
	 * @param  wsvCn      The connection to cvs data source file.
	 * @throws Exception An Exception is trown if the SQL statement is malformed. It also fails if there is one counter 
	 * 						with other columns in the SELECT clause (these should be grouped and this functionality is not 
	 * 						supported at present).
	 */	
	public void parse(String 		  sql,
						   WsvConnection wsvCn) 
	throws Exception {
		String upperCaseSql;
		int    fromPos;
		//		
		upperCaseSql = sql.toUpperCase();
		parseSyntax(upperCaseSql);
		fromPos      = upperCaseSql.lastIndexOf(" FROM ");
		//
		separator    = wsvCn.getSeperator();
      if(separator.equalsIgnoreCase(JdbcSourceConfiguration.FIXED_LENGHT_SEPARATOR))
          setTrimIfString(wsvCn.getTrimIfString());
		tableName    = sql.substring(fromPos + " FROM ".length()).trim();		
		setColumnNames(sql, fromPos, wsvCn);
		setColumnTypes(wsvCn);
		setStringColumnsWidth(wsvCn);
		setFixedLenghtColumnBoundaries(wsvCn);
      setFixedLengthColumnTrims(wsvCn);
	}

	
	private void setColumnNames(String        sql,
										 int 				fromPos,
										 WsvConnection conn) 
	throws Exception {
		String 			 currentToken, counterToken;
		Vector 			 cols;
		StringTokenizer tokenizer;		
		//		
		colsIncludeAtLeastAcounter = false;										
		cols 		 = new Vector();      
		tokenizer = new StringTokenizer(sql.substring(7, fromPos), String.valueOf(",")); // ',' is the separator for SQL syntax
		while (tokenizer.hasMoreTokens()) {
			// column names are left as they are: no uppercasing (except for COUNT clauses)
			currentToken = tokenizer.nextToken().trim();
			counterToken = SqlCounterFakeType.getItemToBeCounted(currentToken, null); 
			if(counterToken != null){
				cols.add("COUNT(" + counterToken + ")"); // in well formatted way 
				colsIncludeAtLeastAcounter = true;
			}
			else{
				cols.add(currentToken);
			}         
		}		
		if (colsIncludeAtLeastAcounter){
			if(cols.size()>1){
				//NOTE: at present it's not considered the case with more than one COUNT clause!
				throw new Exception("Malformed SQL: the list of attributes includes the COUNT(*) clause and some more attributes. \n" +
										  "Attributes other than COUNT(*) should be GROUPed and, at the moment, this feature is not allowed.");
			}
		}
		columnNames = new String[cols.size()];
		cols.copyInto(columnNames);
	}


	/**
	 * If there's a COUNT clause then the list of column types is updated so that it includes a fake column type (SqlCounterFakeType).
	 * @pre_condition It's assumed that columnNames is well defined (not empty)
	 * @param conn
	 */
	private void setColumnTypes(WsvConnection conn) {
		Class[] colTypes;
		Class   type;
		//		
		columnTypes = new Class[columnNames.length];
		colTypes    = conn.getColumnTypes();		
		for (int i = 0; i < columnNames.length; i++) {
			if (StringUtilities.isInString(columnNames[i], "COUNT(")) {
				columnTypes[i] = SqlCounterFakeType.class;
			}
			else 
				if(columnNames[i].equals("*"))
					columnTypes[i] = SqlEveryRecordFakeType.class;
				else {
					if(colTypes != null){
						type           = conn.getColumnTypeByName(columnNames[i]);
						columnTypes[i] = type != null ? type: String.class; // VARCHAR is assumed as default
					}
					else
						columnTypes[i] = String.class;
			}
		}						
	}			


	/**
	 * @pre_condition It's assumed that columnNames is well defined (not empty)
	 */
	private void setStringColumnsWidth(WsvConnection conn){
		stringTypedColumnWidths = conn.getColumnWidths();
	}


	private void setFixedLenghtColumnBoundaries(WsvConnection conn) 
	throws Exception{
		String[] boundaries;
		//		
		boundaries = conn.getColumnBounds();		
		if (boundaries != null) {
			columnBeginBounds = new Integer[boundaries.length];
			columnEndBounds   = new Integer[boundaries.length];
			for (int i = 0; i < boundaries.length; i++) {
				columnBeginBounds[i] = conn.getColumnBeginBound(i);
				columnEndBounds[i] = conn.getColumnEndBound(i);
			}
		}
		else if (separator.trim().equals(JdbcSourceConfiguration.FIXED_LENGHT_SEPARATOR)) {
			throw new Exception("Invalid data specification: using a fixed lenght separator without any valid fixed lenght specification!");
		}		
	}
    
   private void setFixedLengthColumnTrims(WsvConnection conn)
   throws Exception{
      boolean[] trimEnablings = conn.getTrimIfString();        
      if (trimEnablings != null) {
         setTrimIfString(trimEnablings);         
      }      
   }
		
	public boolean colsIncludeCounter() {
		return colsIncludeAtLeastAcounter;
	}

   public boolean[] getTrimIfString() {
      return trimIfString;
   }

   private void setTrimIfString(boolean[] trimIfString) {
      this.trimIfString = trimIfString;
   }
}
